Skip to content

iter17: A/B regression audit — Phases 0-2 - #14

Open
bfirestone wants to merge 19 commits into
mainfrom
iter17-ab-regression-audit
Open

iter17: A/B regression audit — Phases 0-2#14
bfirestone wants to merge 19 commits into
mainfrom
iter17-ab-regression-audit

Conversation

@bfirestone

@bfirestone bfirestone commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Summary

iter17 is an A/B regression audit that gates iteration 18's full 90k annotation run on proof that iteration 16's longer SYSTEM_PROMPT did not silently degrade any of the 40 pre-existing detection labels.

This PR delivers Phases 0-2 of the implementation plan (11 of 18 tasks): all code, tests, and the new trainr data compare-prompts CLI. Phase 3-5 (~$45 OpenRouter annotation runs + regression report + human gate decision) is deferred to a future session — see docs/SESSION_HANDOFF_iter17_phase2_complete.md for the exact resume point.

Total commits on branch: 18 (4 design/plan + 13 code commits with TDD and two-stage review on each).

What's in this PR

Design (4 commits)

  • Spec: docs/superpowers/specs/2026-04-10-iter17-ab-regression-audit-design.md
  • Plan: docs/superpowers/plans/2026-04-10-iter17-ab-regression-audit.md
  • Both went through SPEC_REVIEW and PLAN_REVIEW cycles catching 5+5 findings before any code was written.

Phase 0 — Operational Fixes

  • 7eafced__main__ guard on annotate_detections.py (restores python -m backup path)
  • 497588c — Tightened INJECTION_PATTERNS["stack_trace"] for Python/Java/Rust false positives. Polars-compatible patterns ([\s\S]{0,N} instead of (?s:.), (?m)^ instead of lookbehind). Does NOT rebuild iter16_5k_input.parquet.

Phase 1 — Audit Helpers (audit_semantic_labels.py)

  • 8f1ef7d + 81adffccompute_prevalence_per_label() (majority-of-N fire rate, filters internally)
  • 99159b4 + 0d67183load_annotator_parquets() + EXPECTED_MODEL_SLUGS (filename slug parsing with set-equality validation; rejects wrong count, duplicates, unknown slugs)
  • e71e16c — Byte-for-byte reproducibility smoke test for the iter16 audit report (guards against silent drift)

Phase 2 — New Module compare_prompt_versions.py

  • 2b78826 — Skeleton + DeltaReport/LabelRow/LabelVerdict/LabelCategory types + happy-path test
  • 37383a8 — Dynamic det_* column categorization (shared / iter15-only / iter16-only) + hard schema assertion between after and noise_floor frame sets
  • 445751c — Verdict logic: hard FAIL-agreement (|Δagr| > 0.005) + soft WARN-prevalence (prev_ratio ∉ [0.5, 2.0]) + co-occurrence (FAIL_AND_WARN) + zero-prevalence rules (0/0 → 1.0 PASS, 0/>0 → inf WARN)
  • 0846735 — Row-alignment fingerprint validation (hashes all non-det_* columns row-by-row against input parquet; raises ValueError naming first diverging row index, column, and source slug)
  • 739757f — Pure markdown format_delta_report() with all 8 sections from the spec's §Report Schema, including FAIL-agreement detail with override-eligibility hint (|Δagr| / noise_floor ratio)
  • 62cbd6dtrainr data compare-prompts CLI wire-up with internal glob expansion via Python glob module (shell-independent). Exits 2 on any FAIL-agreement (report is written before exit).

Test coverage

File New tests What
tests/test_compare_prompt_versions.py 24 Happy path, 4 categorization, 7 verdict, 4 fingerprint, 3 format, 3 CLI, 2 constants
tests/test_audit_semantic_labels.py +12 Prevalence (5), loader (6), iter16 reproducibility smoke (1)
tests/test_build_audit_sample.py +6 Stack-trace injection tightening regressions (3 anti-cases + 3 positive cases)

All tests green. An end-to-end smoke test of trainr data compare-prompts against real iter16 parquets (running iter16 vs iter16 as both sides) produced a valid PASS report with all 42 shared labels and zero deltas — confirming the full CLI code path works against real data.

Why paused before Phase 3

Phase 3 runs ~$45 of OpenRouter annotation calls (iter15 ×3 models + iter16a ×3 + iter16b ×3 noise floor). Phase 4 writes the iteration doc which requires human judgment on the regression report. Phase 5 is the gate decision per the Gate Decision Protocol. These need explicit user go-ahead and interactive error handling, so they're better run from a fresh session with the user present rather than as a long-running subagent chain. The handoff doc captures exact resume commands.

Key design decisions (from spec+plan)

  1. Full re-run both sides, not "trust archived iter16 parquets" — the archived parquets pre-date the c1ec175 CSV-log refinement, so they measure prompt drift rather than same-prompt variance.
  2. Dedicated iter16b companion run for noise floor — |agr(iter16a) - agr(iter16b)| with both on the iter16 prompt at HEAD. FAIL-path iteration reruns both iter16a AND iter16b (~$30 per iteration) — caching iter16b against a new iter16a prompt would silently corrupt the noise floor.
  3. Fingerprint row alignmentiter16_5k_input.parquet has 5065 rows but only 5059 unique text values, so text-only equality is insufficient. Hashes all non-det_* columns.
  4. Model slug set validation{gemini3flash, sonnet, gpt54mini} is a canonical frozenset; filename regex + set-equality catches duplicates and misnames that would silently corrupt agreement estimates.
  5. Override eligibility is |Δagr| ≤ 2 × noise_floor — noise floor bounds override eligibility but does NOT adjust the 0.005 threshold itself. Longitudinal consistency vs per-run noise calibration.

Test plan (reviewer)

  • All 24 tests in test_compare_prompt_versions.py pass
  • trainr data compare-prompts --help renders all 5 options
  • End-to-end smoke test produces a valid PASS markdown report on real iter16 parquets
  • No regressions in test_audit_semantic_labels.py / test_build_audit_sample.py / test_annotate_detections.py
  • iter16_5k_input.parquet mtime unchanged (file not rebuilt)
  • Byte-for-byte reproducibility of iter16 audit report after Phase 1 changes
  • Phase 3-5 deferred: real annotation runs, regression report generation, gate decision

Out of scope (iter18+)

  • iter18: 90k annotation run + DVC push
  • iter19: retrain with detection head + post-retrain eval gate
  • iter20: Rust port (artifact refresh + smoke test)

See docs/SESSION_HANDOFF_iter17_phase2_complete.md for the exact resume point when picking up Phase 3 in a future session.

Gates iter18's $400-600 90k annotation run on proof that iter16's
longer SYSTEM_PROMPT did not silently regress any pre-existing
detection label. Full re-run of both prompt versions on the same
5065-row input, tri-categorized report (shared/iter15-only/
iter16-only), hard agreement gate + soft prevalence warning +
informational noise floor.
- Replace archived iter16 parquets as noise-floor source with a
  dedicated iter16b re-run (prior framing measured prompt drift from
  c1ec175, not same-prompt variance).
- Rewrite FAIL path to reflect that annotator emits all det_* columns
  in a single call, so any prompt iteration requires a full iter16-side
  rerun (~$15); only the report filters to regressed labels.
- Specify fingerprint-based row alignment against input parquet
  (non-det_* columns), since 5065 rows contain only 5059 unique text
  values.
- Drop "noise floor informational only" framing; noise floor now
  formally bounds override eligibility via |Δagr| ≤ 2 × noise_floor.
- Cap annotation concurrency at 2 jobs in flight (40 LLM requests)
  instead of running all sides in parallel.
- Type hints use pl.DataFrame, not pd.DataFrame, to match the
  existing polars-only audit stack.
- Expand CLI globs internally via Python glob, not the shell; assert
  each glob resolves to exactly 3 parquets.
- Total annotation cost: ~$45 (iter15 ×1 + iter16 ×2).
- FAIL-path iteration reruns both iter16a AND iter16b (~$30), not
  just iter16a. Reusing cached iter16b against a new iter16a prompt
  would turn the "noise floor" back into prompt-drift measurement —
  the exact mistake the Codex review caught for archived parquets,
  repeated at a different layer.
- Stratified-row filter uses audit_source == "stratified"
  explicitly; base cohort gets that value and injected rows use
  inject_<label>, so the original audit_source=injected phrasing
  would have included injected rows in agreement/prevalence metrics.
- prev_ratio 0/0 is now explicitly defined as 1.0 (no WARN), with
  a table for the other zero combinations. A silent-on-both-sides
  label no longer trips WARN-prevalence.
- CLI glob validation now parses filenames and asserts the model
  slug set matches the canonical {gemini3flash, sonnet46, gpt54mini}
  set rather than just counting 3 parquets. Catches duplicates and
  misnamed files that would silently corrupt agreement estimates.
- after and noise_floor det_* column sets are hard-asserted equal,
  not "assumed equal by construction." Override eligibility depends
  on noise floor so a schema mismatch must fail loud.
- Added unit test cases for each new validation.
Comprehensive task-by-task TDD plan for the iter17 A/B regression
audit. 16 tasks across 6 phases (P0 ops fixes → P1 audit helpers →
P2 compare_prompt_versions.py module → P3 annotation runs → P4
report generation → P5 gate decision).

Also aligns spec model slug to existing 'sonnet' convention
(not 'sonnet46'), matching the filename pattern of the existing
iter16_5k_sonnet.parquet artifact.
Restores python -m trainr.core.annotate_detections as a working
invocation path. iter16 lost ~30 min diagnosing a silent exit 0
when running via python -m; the click CLI was always the canonical
path but the backup diagnostic route was broken.
iter16 found 9 of 9 stack_trace 'misses' were actually regex
overmatches on Rust test directives, prose with 'at line N', and
Java frames without Exception headers. Tighten:

- Python: require 'Traceback (...)' header + 'File "' adjacency
- Java:   require 'Exception in thread' header + java-frame adjacency
- Rust:   (?m)^ line anchor excludes '// error-pattern:' directives

Patterns use [\s\S]{0,N} instead of (?s:.{0,N}) and (?m)^ instead
of negative lookbehind, because polars uses Rust's regex crate
which rejects both of the latter constructs.

Adds regression tests in both directions — previously-matching
anti-cases no longer match, real traces still match. Does not
rebuild iter16_5k_input.parquet.
Per-label majority-of-N fire rate on stratified rows. Needed by
the iter17 A/B compare module's soft WARN-prevalence gate. Uses
the same dict[model_slug, DataFrame] convention as the existing
compute_agreement_across_models. Filters internally so callers
don't need to remember to pre-filter to stratified rows.
Adds a test for the "label in first frame but not others" case
that exercises the continue branch in compute_prevalence_per_label.
Was flagged as a minor coverage gap in code review of 8f1ef7d.
Parses model slug from filename and asserts the parsed set equals
the canonical {gemini3flash, sonnet, gpt54mini} set. Rejects wrong
count, unparseable names, duplicates, and unknown slugs. Shared by
both the existing audit_semantic_labels.main() and the upcoming
compare_prompt_versions module.

The EXPECTED_MODEL_SLUGS frozenset is the single source of truth
for the canonical slug set.
Adds a test where two paths in different directories both parse
to the same canonical slug (gemini3flash). Exercises the
"if slug in parsed" branch in load_annotator_parquets that was
flagged as a minor coverage gap in review of 99159b4.
Regenerates the iter16 audit report from the archived parquets
and diffs against the committed docs/accuracy_runs report. Fails
loud on any silent drift in agreement math, rounding, or output
format. Insurance for the Phase 1 refactor + any future edit.
Defines LabelCategory, LabelVerdict, LabelRow, and DeltaReport
dataclasses. The compare_prompt_versions() function handles the
all-shared happy-path case with placeholder PASS verdicts.
_compute_prev_ratio implements the spec's zero-handling rules
(0/0 → 1.0, 0/>0 → inf, >0/0 → 0.0).

Verdict logic, column categorization, fingerprint validation, and
report emission arrive in subsequent tasks.
Dynamic det_* introspection categorizes every label as
shared/iter15-only/iter16-only. Shared labels get full metrics;
the other two categories get partial metrics and NO_VERDICT.
Hard-asserts after and noise_floor have identical det_* column
sets — the override criterion depends on noise floor, so a
schema mismatch must fail loud rather than silently skip.
Implements _compute_verdict() combining the hard agreement gate
(|Δagr| > 0.005) with the soft prevalence gate (prev_ratio outside
[0.5, 2.0]). Verdicts can co-occur (FAIL_AND_WARN). Includes
tests for each path including the zero-prevalence edge cases.

Override eligibility (|Δagr| ≤ 2 × noise_floor) is NOT applied
here. The verdict reflects the raw gate outcome; human review
applies override logic per the Gate Decision Protocol.
Validates every annotation frame against the input parquet by
comparing all non-det_* columns row-for-row. Raises ValueError
naming the first diverging row index, column, and source slug
on mismatch.

Needed because iter16_5k_input.parquet has 5065 rows but only
5059 unique 'text' values — text-only equality is insufficient
as an alignment check.

Fingerprint runs BEFORE the schema assertion so row drift is
caught with a precise row index rather than a vague column-set
diff.
format_delta_report renders the DeltaReport with all sections
from the spec: gate verdict summary, shared labels table,
iter15-only and iter16-only sections, FAIL-agreement detail
with override-eligibility hint (|Δagr|/noise_floor ratio),
WARN-prevalence detail, and a noise floor table covering all
categorized labels.
Wires compare_prompt_versions into the trainr data click group.
Module main() parses argv, resolves globs via Python glob module
(not the shell), loads parquets via load_annotator_parquets with
slug validation, runs the comparison, and writes the markdown
report. Exits 2 on any FAIL-agreement verdict so shell callers
can gate on it.
Code work (Phases 0-2, 11 tasks) is done and tested. Remaining
work (Phases 3-5, ~$45 LLM budget + human gate review) picked up
in a future session. Handoff doc captures:

- Completed task list with commit SHAs
- Test coverage summary (24 compare tests + 14 audit + 6 build_audit)
- Exact commands to resume at Task 3.1 (worktree setup)
- Gate decision protocol reference
- Pyright noise context + 10 unrelated pre-existing failures note
- FAIL-path cost note ($30 per iteration, iter15 cached)
@bfirestone bfirestone changed the title iter17: A/B regression audit — Phases 0-2 (code only, paused before annotation runs) iter17: A/B regression audit — Phases 0-2 Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant